﻿/*
 * DotNetCrypt - an open source library of cryptographic algorithms for .NET
 * Copyright (C) 2009 David Musgrove
 * 
 * This file is part of DotNetCrypt.
 *
 * DotNetCrypt is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * DotNetCrypt is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;

namespace DotNetCrypt
{
// ReSharper disable InconsistentNaming
    /// <summary>
    /// Performs a cryptographic transformation of data using the
    /// <see cref="RC5" /> algorithm. This class cannot be inherited.
    /// </summary>
    public sealed class RC5ManagedTransform : ManagedTransformBase
// ReSharper restore InconsistentNaming
    {
        private readonly byte _rounds;
        private readonly int _blockSize;

        private ushort[] _key16;
        private uint[] _key32;
        private ulong[] _key64;

        private const ushort P16 = 0xb7e1;
        private const ushort Q16 = 0x9e37;
        private const uint P32 = 0xb7e15163;
        private const uint Q32 = 0x9e3779b9;
        private const ulong P64 = 0xb7e151628aed2a6b;
        private const ulong Q64 = 0x9e3779b97f4a7c15;

        internal RC5ManagedTransform(ISymmetricAlgorithm algorithm, byte[] rgbKey, byte[] rgbIv, TransformDirection transformDirection)
            : base(algorithm, rgbIv, transformDirection, Endianness.Little)
        {
            Mode = algorithm.ExtendedMode;
            _rounds = ((RC5)algorithm).Rounds;
            _blockSize = algorithm.BlockSize;
            switch (_blockSize)
            {
                case 32:
                    ExpandKey16(rgbKey);
                    break;
                case 64:
                    ExpandKey32(rgbKey);
                    break;
                case 128:
                    ExpandKey64(rgbKey);
                    break;
            }
        }

        internal bool IsWeakKey
        {
            get
            {
                return false;
            }
        }

        private void ExpandKey16(byte[] key)
        {
            int arrayLength = 2 * (_rounds + 1);
            _key16 = new ushort[arrayLength];
            var fullBlockKey = new byte[4 * ((key.Length + 3) / 4)];
            key.CopyTo(fullBlockKey, 0);
            ushort[] fullBlockKeyWords = Utils.BytesTo16BitWordsLittleEndian(fullBlockKey);
            _key16[0] = P16;
            for (int index = 1; index < arrayLength; index++)
            {
                _key16[index] = (ushort)(_key16[index - 1] + Q16);
            }
            int rounds = 3 * Math.Max(arrayLength, fullBlockKeyWords.Length);
            int i = 0;
            int j = 0;
            ushort tempA = 0;
            ushort tempB = 0;
            for (int counter = 0; counter < rounds; counter++)
            {
                _key16[i] = (ushort)(_key16[i] + tempA + tempB);
                _key16[i] = (ushort)((_key16[i] << 3) | (_key16[i] >> 13));
                tempA = _key16[i];
                var temp = (ushort)(tempA + tempB);
                fullBlockKeyWords[j] = (ushort)(fullBlockKeyWords[j] + temp);
                fullBlockKeyWords[j] = (ushort)((fullBlockKeyWords[j] << (temp % 16)) | (fullBlockKeyWords[j] >> (16 - (temp % 16))));
                tempB = fullBlockKeyWords[j];
                i = (i + 1) % arrayLength;
                j = (j + 1) % fullBlockKeyWords.Length;
            }
        }

        private void ExpandKey32(byte[] key)
        {
            int arrayLength = 2 * (_rounds + 1);
            _key32 = new uint[arrayLength];
            var fullBlockKey = new byte[4 * ((key.Length + 3) / 4)];
            key.CopyTo(fullBlockKey, 0);
            uint[] fullBlockKeyWords = Utils.BytesToWordsLittleEndian(fullBlockKey);
            _key32[0] = P32;
            for (int index = 1; index < arrayLength; index++)
            {
                _key32[index] = _key32[index - 1] + Q32;
            }
            int rounds = 3 * Math.Max(arrayLength, fullBlockKeyWords.Length);
            int i = 0;
            int j = 0;
            uint tempA = 0;
            uint tempB = 0;
            for (int counter = 0; counter < rounds; counter++)
            {
                _key32[i] = _key32[i] + tempA + tempB;
                _key32[i] = (_key32[i] << 3) | (_key32[i] >> 29);
                tempA = _key32[i];
                uint temp = tempA + tempB;
                fullBlockKeyWords[j] = fullBlockKeyWords[j] + temp;
                fullBlockKeyWords[j] = (fullBlockKeyWords[j] << (int)(temp % 32)) | (fullBlockKeyWords[j] >> (int)(32 - (temp % 32)));
                tempB = fullBlockKeyWords[j];
                i = (i + 1) % arrayLength;
                j = (j + 1) % fullBlockKeyWords.Length;
            }
        }

        private void ExpandKey64(byte[] key)
        {
            int arrayLength = 2 * (_rounds + 1);
            _key64 = new ulong[arrayLength];
            var fullBlockKey = new byte[4 * ((key.Length + 3) / 4)];
            key.CopyTo(fullBlockKey, 0);
            ulong[] fullBlockKeyWords = Utils.BytesTo64BitWordsLittleEndian(fullBlockKey);
            _key64[0] = P64;
            for (int index = 1; index < arrayLength; index++)
            {
                _key64[index] = (_key64[index - 1] + Q64);
            }
            int rounds = 3 * Math.Max(arrayLength, fullBlockKeyWords.Length);
            int i = 0;
            int j = 0;
            ulong tempA = 0;
            ulong tempB = 0;
            for (int counter = 0; counter < rounds; counter++)
            {
                _key64[i] = _key64[i] + tempA + tempB;
                _key64[i] = (_key64[i] << 3) | (_key64[i] >> 61);
                tempA = _key64[i];
                ulong temp = tempA + tempB;
                fullBlockKeyWords[j] = (fullBlockKeyWords[j] + temp);
                fullBlockKeyWords[j] = (ulong)((fullBlockKeyWords[j] << (int)(temp % 64)) | (fullBlockKeyWords[j] >> (int)(64 - (temp % 64))));
                tempB = fullBlockKeyWords[j];
                i = (i + 1) % arrayLength;
                j = (j + 1) % fullBlockKeyWords.Length;
            }
        }

        /// <summary>
        /// Performs the encryption transformation on a block of bytes that
        /// have been translated into words using the big endian convention.
        /// </summary>
        /// <param name="plain">
        /// The words to encrypt.
        /// </param>
        [CLSCompliant(false)]
        protected internal override void Encrypt(uint[] plain)
        {
            uint plain0 = plain[0] + _key32[0];
            uint plain1 = plain[1] + _key32[1];
            for (int i = 1; i <= _rounds; i++)
            {
                plain0 ^= plain1;
                plain0 = (plain0 << (int)(plain1 % 32)) | (plain0 >> (int)(32 - (plain1 % 32)));
                plain0 += _key32[2 * i];
                plain1 ^= plain0;
                plain1 = (plain1 << (int)(plain0 % 32)) | (plain1 >> (int)(32 - (plain0 % 32)));
                plain1 += _key32[2 * i + 1];
            }
            plain[0] = plain0;
            plain[1] = plain1;
        }

        /// <summary>
        /// Performs the decryption transformation on a block of bytes that
        /// have been translated into words using the big endian convention.
        /// </summary>
        /// <param name="cipher">
        /// The words to decrypt.
        /// </param>
        [CLSCompliant(false)]
        protected internal override void Decrypt(uint[] cipher)
        {
            uint cipher0 = cipher[0];
            uint cipher1 = cipher[1];
            for (int i = _rounds; i >= 1; i--)
            {
                cipher1 -= _key32[2 * i + 1];
                cipher1 = (cipher1 >> (int)(cipher0 % 32)) | (cipher1 << (int)(32 - (cipher0 % 32)));
                cipher1 ^= cipher0;
                cipher0 -= _key32[2 * i];
                cipher0 = (cipher0 >> (int)(cipher1 % 32)) | (cipher0 << (int)(32 - (cipher1 % 32)));
                cipher0 ^= cipher1;
            }
            cipher[0] = cipher0 - _key32[0];
            cipher[1] = cipher1 - _key32[1];
        }

        /// <summary>
        /// Encrypts a single block of bytes, writing the result into the
        /// provided array.
        /// </summary>
        /// <param name="block">
        /// The block of bytes to be encrypted.
        /// </param>
        protected override void EncryptBlock(byte[] block)

        {
            switch (block.Length)
            {
                case 4:
                    ushort[] shorts = Utils.BytesTo16BitWordsLittleEndian(block);
                    Encrypt(shorts);
                    Utils.Write16BitWordsIntoBytesLittleEndian(shorts, block);
                    break;
                case 8:
                    uint[] ints = Utils.BytesToWordsLittleEndian(block);
                    Encrypt(ints);
                    Utils.WriteWordsIntoBytesLittleEndian(ints, block);
                    break;
                case 16:
                    ulong[] longs = Utils.BytesTo64BitWordsLittleEndian(block);
                    Encrypt(longs);
                    Utils.Write64BitWordsIntoBytesLittleEndian(longs, block);
                    break;
            }
        }

        private void Encrypt(ulong[] plain)
        {
            ulong plain0 = plain[0] + _key64[0];
            ulong plain1 = plain[1] + _key64[1];
            for (int i = 1; i <= _rounds; i++)
            {
                plain0 ^= plain1;
                plain0 = (plain0 << (int)(plain1 % 64)) | (plain0 >> (int)(64 - (plain1 % 64)));
                plain0 += _key64[2 * i];
                plain1 ^= plain0;
                plain1 = (plain1 << (int)(plain0 % 64)) | (plain1 >> (int)(64 - (plain0 % 64)));
                plain1 += _key64[2 * i + 1];
            }
            plain[0] = plain0;
            plain[1] = plain1;
        }

        /// <summary>
        /// Decrypts a single block of bytes, writing the result into the
        /// provided array.
        /// </summary>
        /// <param name="block">
        /// The block of bytes to be decrypted.
        /// </param>
        protected override void DecryptBlock(byte[] block)
        {
            switch (block.Length)
            {
                case 4:
                    ushort[] shorts = Utils.BytesTo16BitWordsLittleEndian(block);
                    Decrypt(shorts);
                    Utils.Write16BitWordsIntoBytesLittleEndian(shorts, block);
                    break;
                case 8:
                    uint[] ints = Utils.BytesToWordsLittleEndian(block);
                    Decrypt(ints);
                    Utils.WriteWordsIntoBytesLittleEndian(ints, block);
                    break;
                case 16:
                    ulong[] longs = Utils.BytesTo64BitWordsLittleEndian(block);
                    Decrypt(longs);
                    Utils.Write64BitWordsIntoBytesLittleEndian(longs, block);
                    break;
            }
        }

        private void Decrypt(ulong[] cipher)
        {
            ulong cipher0 = cipher[0];
            ulong cipher1 = cipher[1];
            for (int i = _rounds; i >= 1; i--)
            {
                cipher1 -= _key64[2 * i + 1];
                cipher1 = (cipher1 >> (int)(cipher0 % 64)) | (cipher1 << (int)(64 - (cipher0 % 64)));
                cipher1 ^= cipher0;
                cipher0 -= _key64[2 * i];
                cipher0 = (cipher0 >> (int)(cipher1 % 64)) | (cipher0 << (int)(64 - (cipher1 % 64)));
                cipher0 ^= cipher1;
            }
            cipher[0] = cipher0 - _key64[0];
            cipher[1] = cipher1 - _key64[1];
        }

        private void Encrypt(ushort[] plain)
        {
            var plain0 = (ushort)(plain[0] + _key16[0]);
            var plain1 = (ushort)(plain[1] + _key16[1]);
            for (int i = 1; i <= _rounds; i++)
            {
                plain0 ^= plain1;
                plain0 = (ushort)((plain0 << (plain1 % 16)) | (plain0 >> (16 - (plain1 % 16))));
                plain0 += _key16[2 * i];
                plain1 ^= plain0;
                plain1 = (ushort)((plain1 << (plain0 % 16)) | (plain1 >> (16 - (plain0 % 16))));
                plain1 += _key16[2 * i + 1];
            }
            plain[0] = plain0;
            plain[1] = plain1;
        }

        private void Decrypt(ushort[] cipher)
        {
            ushort cipher0 = cipher[0];
            ushort cipher1 = cipher[1];
            for (int i = _rounds; i >= 1; i--)
            {
                cipher1 -= _key16[2 * i + 1];
                cipher1 = (ushort)((cipher1 >> (cipher0 % 16)) | (cipher1 << (16 - (cipher0 % 16))));
                cipher1 ^= cipher0;
                cipher0 -= _key16[2 * i];
                cipher0 = (ushort)((cipher0 >> (cipher1 % 16)) | (cipher0 << (16 - (cipher1 % 16))));
                cipher0 ^= cipher1;
            }
            cipher[0] = (ushort)(cipher0 - _key16[0]);
            cipher[1] = (ushort)(cipher1 - _key16[1]);
        }

        /// <summary>
        /// Clears all potentially sensitive data stores.
        /// </summary>
        protected internal override void Reset()
        {
            if (_key16 != null)
            {
                Array.Clear(_key16, 0, _key16.Length);
            }
            if (_key32 != null)
            {
                Array.Clear(_key32, 0, _key32.Length);
            }
            if (_key64 != null)
            {
                Array.Clear(_key64, 0, _key64.Length);
            }
            base.Reset();
        }
    }
}